#!/usr/bin/env python

#
# Logging manager singleton.
#

import re
import sys
import os.path
import logging
from dm.common.utility.configurationManager import ConfigurationManager
from dm.common.exceptions.configurationError import ConfigurationError

class LoggingManager:
    """ 
    The log manager class is initialized via a configuration file 
    that may have the following sections:

    ConsoleLogging      # Used for output on the screen
    FileLogging         # Used for logging into a file

    Each section in the configuration file should have the following 
    keys:
        handler     # Indicates which handler class to use 
        level       # Indicates logging level
        format      # Indicates format for log messages
        dateFormat  # Indicates date format used for log messages

    Given below is an example of a valid configuration file:

    [ConsoleLogging]
        handler=ConsoleLoggingHandler(sys.stdout,)
        level=info
        format=[%(levelname)s] %(message)s 
        dateFormat=%m/%d/%y %H:%M:%S
    [FileLogging]
        handler=TimedRotatingFileLoggingHandler('/tmp/dm.log')
        level=debug
        format=%(asctime)s,%(msecs)d [%(levelname)s] %(module)s:%(lineno)d %(user)s@%(host)s %(name)s (%(process)d): %(message)s
        dateFormat=%m/%d/%y %H:%M:%S
    """

    # Get singleton instance.
    @classmethod
    def getInstance(cls):
        from dm.common.utility.loggingManager import LoggingManager
        try:
            lm = LoggingManager()
        except LoggingManager, ex:
            lm = ex
        return lm

    # Singleton.
    __instance = None

    def __init__(self):
        if LoggingManager.__instance:
             raise LoggingManager.__instance
        LoggingManager.__instance = self
        self.consoleHandler = None
        self.fileHandlerList = []
        self.maxIntLevel = logging.CRITICAL
        self.minIntLevel = logging.NOTSET
        self.levelRegExList = []
        self.logger = logging.getLogger(self.__class__.__name__)
        self.initFlag = False

    def setMinLogLevel(self, minLogLevel=logging.INFO):
        self.minIntLevel = minLogLevel

    def parseLevelRegEx(self, levelRegExList):
        """ Parse expressions of the form <regex>=<log level>. """
        lines = levelRegExList.split('\n')
        for line in lines:
            try:
                 (regex, level) = line.rsplit('=', 1)
                 pattern = re.compile(regex)
                 tuple = (pattern, logging.getLevelName(level.upper()))
                 self.levelRegExList.append(tuple)
            except Exception, ex:
                 self.logger.error('Parser error in log configuration file: %s' % line)
                 self.logger.exception(ex)

    # Get Log Level based on a string representation
    def getIntLogLevel(self, levelStr):
        level = logging.getLevelName(levelStr)
        # Level should be an integer
        try:
            return int(level) 
        except ValueError, ex:
            raise ConfigurationError('"%s" is not valid log level' % levelStr)

    # Configure log handlers.
    def configure(self):
        """ Configure log handlers from the config file. """
        cm = ConfigurationManager.getInstance()
        configFile = cm.getConfigFile()
        configParser = cm.getConfigParserFromConfigFile(configFile)
        configSections = cm.getConfigSectionsFromConfigParser(configParser)

        # Console handler.
        defaults = { 
           'level' : cm.getConsoleLogLevel(),
           'format' : cm.getLogRecordFormat(),
           'dateFormat' : cm.getLogDateFormat(),
           'handler' : 'ConsoleLoggingHandler(sys.stdout,)'
        }
        if not self.consoleHandler:
            consoleHandler = self.__configureHandler(configParser, 'ConsoleLogging', defaults)

            if consoleHandler != None:
                self.consoleHandler = consoleHandler

        # File logging. 
        # Do not configure if log directory does not exist.
        defaults['handler'] = None
        defaults['level'] = cm.getFileLogLevel() 
        if not os.path.exists(configFile):
            # No config file, we'll configure default.
            defaultLogFile = cm.getLogFile()
            defaultLogDir = os.path.dirname(defaultLogFile)
            if os.path.exists(defaultLogDir):
                handler = 'TimedRotatingFileLoggingHandler("%s")' % defaultLogFile 
                defaults['handler'] = handler
                fileHandler = self.__configureHandler(configParser, 'FileLogging', defaults)
                if fileHandler != None:
                    self.fileHandlerList.append(fileHandler)

        else:
            # Parse all file loggers present in the config file
            for configSection in configSections:
                if configSection.startswith('FileLogging'):
                     fileHandler = self.__configureHandler(configParser, configSection, defaults)
                     if fileHandler != None:
                         self.fileHandlerList.append(fileHandler)

        # Add handlers to the root logger.  Use logging class here
        # to make sure we can have a logger when we parse the 
        # logger expressions
        rootLogger = logging.getLogger('')
        for handler in [self.consoleHandler] + self.fileHandlerList:
            rootLogger.addHandler(handler)

        # Get a logger factory based on our current config 
        self.configureLoggers(configParser, defaultLevel='debug')

    def configureLoggers(self, configParser, defaultLevel='error'):
        rootLogLevel = 'error'
        levelRegEx = '^.*$=%s' % (defaultLevel)
        if configParser is not None and configParser.has_section('LoggerLevels'):
            rootLogLevel = ConfigurationManager.getOptionFromConfigParser(configParser, 'LoggerLevels', 'root', rootLogLevel)
            levelRegEx = ConfigurationManager.getOptionFromConfigParser(configParser, 'LoggerLevels', 'levelregex', levelRegEx)

        rootLevelInt = logging.getLevelName(rootLogLevel.upper())
        logging.getLogger('').root.setLevel(rootLevelInt)
        logging.getLogger('').debug('Set root logger to %s' % rootLevelInt)

        if not levelRegEx:
            return

        # Parse expressions of the form <regex>=<log-level>. """
        lines = levelRegEx.split('\n')
        for line in lines:
            try:
                # Use the right split so we can have '='s in the regex
                (regex, level) = line.rsplit('=', 1)
                pattern = re.compile(regex)
                tuple = (pattern, logging.getLevelName(level.upper()))
                self.levelRegExList.append(tuple)
            except Exception, ex:
                # Do not fail
                self.logger.error('Parser error in log configuration file: %s' % line)
                self.logger.exception(ex)

    # Configure particular handler with given defaults.
    def __configureHandler(self, configParser, configSection, defaults):
        """ Configure specified handler with a given defaults. """
        handlerOption = defaults['handler']
        try:
            if configParser is not None:
                handlerOption = configParser.get(configSection, 'handler')
        except Exception, ex:
            pass

        # If handlerOption is empty, handler cannot be instantiated.
        handler = None
        if handlerOption != None:
            # Handler argument format: MyHandler(arg1, arg2, ...)
            # Module will be in lowercase letters, but the class
            # should be capitalized.
            handlerName = re.sub('\(.*', '', handlerOption)
            moduleName = handlerName[0].lower() + handlerName[1:]
            try:
                exec 'from dm.common.utility import %s' % (moduleName)
                exec 'handler = %s.%s' % (moduleName, handlerOption)
            except IOError, ex:
                errNo, errMsg = ex
                import errno

                # If the exception raised is an I/O permissions error, ignore
                # it and disable this log handler.  This allows non-root users
                # to use the (system-wide) default log configuration
                if errNo != errno.EACCES:
                    raise
                handler = None 
            except Exception, ex:
                raise ConfigurationError(exception=ex)

        # Only request setting from the config file if it was
        # not set via environment variable, or programmatically.
        cm = ConfigurationManager.getInstance()
        if handler != None:
            try:
                level = cm.getOptionFromConfigParser(configParser, configSection, 'level', defaults['level'])
                intLevel = self.getIntLogLevel(level.upper())
                handler.setLevel(intLevel)

                format = cm.getOptionFromConfigParser(configParser, configSection, 'format', defaults['format'])
                dateFormat = cm.getOptionFromConfigParser(configParser, configSection, 'dateFormat', defaults['dateFormat'])

                handler.setFormatter(logging.Formatter(format, dateFormat))
            except Exception, ex:
                raise ConfigurationError(exception=ex)

            # Apply filters to handler
            filter = None
            try:
                filter = configParser.get(configSection, 'filter')
                if filter:
                    handler.addFilter(logging.Filter(filter))
            except Exception, ex:
                pass
        return handler

    def getLogger(self, name='defaultLogger'):
        if not self.initFlag:
            self.initFlag = True
            self.configure()
        logger = logging.getLogger(name)
        logger.setLevel(self.getLevel(name))
        return logger

    def getLevel(self, name):
        # Match from the known regex list.
        level = logging.NOTSET

        # The last regex is most important.
        for e in reversed(self.levelRegExList):
            (pattern, level) = e

            # If we return not None it is a match
            if not None == pattern.match(name):
                break

        if level > self.maxIntLevel:
            level = self.maxIntLevel
        if level < self.minIntLevel:
            level = self.minIntLevel
        return level

    def setConsoleLogLevel(self, level):
        try:
            # We need to override the logger levels and the handler
            intLevel = self.getIntLogLevel(level.upper())
            self.consoleHandler.setLevel(intLevel)
            self.maxIntLevel = intLevel
            self.logger.setLevel(intLevel)
        except Exception, ex:
            raise ConfigurationError(exception=ex)

    def setFileLogLevel(self, level):
        try:
            # We need to override the logger levels and the handler
            intLevel = self.getIntLogLevel(level.upper())
            for handler in self.fileHandlerList:
                handler.setLevel(intLevel)
            self.maxIntLevel = intLevel
            self.logger.setLevel(intLevel)
        except Exception, ex:
            raise ConfigurationError(exception=ex)

#######################################################################
# Testing.

if __name__ == '__main__':
    lm = LoggingManager.getInstance()
    logger = lm.getLogger('Main')
    logger.error('Error In Main')
    logger.debug('Debug In Main')
    logger.warn('Warn In Main')
    logger.info('Info In Main')
    logger = lm.getLogger('Main')
    logger.info('Info In Main')
    logger = lm.getLogger('')
    logger.info('Info using root logger')
    logger = lm.getLogger('Main.2')
    logger.info('Info in Main.2')
    lm.setConsoleLogLevel('info')
    logger.debug('You should not see this message')
    lm.setConsoleLogLevel('debug')
    logger.debug('Debug in Main.2')