#-*-coding:utf-8-*-

#########################################################################
# pyeole.log - logging utils for EOLE project
# Copyright © 2012 Pôle de Compétence EOLE <eole@ac-dijon.fr>
#
# License CeCILL:
#  * in french: http://www.cecill.info/licences/Licence_CeCILL_V2-fr.html
#  * in english http://www.cecill.info/licences/Licence_CeCILL_V2-en.html
#########################################################################

"""Common EOLE log library

This module provides convenient functions to configure logging with
standard :mod:`logging` module.

If no handlers are found on the root logger at import time
:func:`logging.basicConfig` is run to configure it.

A :class:`trace` decorator is provided and can be enabled by setting
:data:`ENABLE_TRACE`, it logs traces at level :data:`logging.DEBUG`.

"""

import sys
import logging
import logging.config

from tempfile import mktemp

# For old API
from pyeole.decorator import advice, deprecated

from pyeole.inspect_utils import get_caller_infos, format_caller


if sys.version_info[0] >= 3:
    unicode = str


ENABLE_TRACE = False
"""Enable tracing globally."""

_HANDLERS_DICT = {u'stdout':
                  {u'class': u'pyeole.loghandlers.ColorizingStreamHandler',
                   u'stream': u'ext://sys.stdout',
                   u'level': u'INFO', # Fixed as best practice
                   u'filters': [u'info'], # Keep only INFO
                   u'formatter': u'brief',},
                  u'stderr':
                  {u'class': u'pyeole.loghandlers.ColorizingStreamHandler',
                   u'stream' : u'ext://sys.stderr',
                   u'level' : u'WARNING', # Fixed as best practice
                   u'filters' : [u'stderr'], # Strip INFO
                   u'formatter' : u'with-name',},
                  u'stddebug':
                  {u'class': u'pyeole.loghandlers.ColorizingStreamHandler',
                   u'stream' : u'ext://sys.stderr',
                   u'level' : u'DEBUG', # Fixed as best practice
                   u'filters' : [u'debug'], # Strip INFO
                   u'formatter' : u'with-name',},
                  u'file' :
                  {u'class': u'logging.FileHandler',
                   u'encoding': u'utf-8',
                   u'filename': mktemp(suffix=u'.log'),
                   u'formatter': u'with-date-name',},
                  u'syslog':
                  {u'class': u'logging.handlers.SysLogHandler',
                   'level': u'DEBUG', # limitted by level argument
                   u'formatter': u'syslog',
                   u'address': u'/run/systemd/journal/dev-log'},
                  u'smtp':
                  {u'class': u'pyeole.loghandlers.SMTPHandler',
                   u'level': u'WARNING',
                   u'mailhost': None,
                   u'fromaddr': None,
                   u'toaddrs': None,
                   u'subject': None,
                   u'secure': (),},
}

# Configuration of formatters
_FORMATTERS_DICT = {u'brief':
                    {u'format': u'%(message)s',
                     u'datefmt': u'', },
                    u'with-name':
                    {u'format': u'%(name)s - %(message)s',
                     u'datefmt': u'', },
                    u'with-levelname-date':
                    {u'format': u'%(asctime)s: %(levelname)s - %(message)s',
                     u'datefmt': u'', },
                    u'with-date-name':
                    {u'format': u'%(asctime)s: %(name)s - %(message)s',
                     u'datefmt' : u'', },
                    u'syslog':
                    {u'format': u'eole: %(name)s - %(message)s',
                     u'datefmt' : u''},
                    u'syslog-named':
                    {u'format': u'%(name)s: %(message)s',
                     u'datefmt' : u''},
}

# Configuration of filters
_FILTERS_DICT = {u'debug':
                 {u'()': u'pyeole.log.LevelsFilter',
                  u'levels': [ logging.DEBUG ],},
                 u'info':
                 {u'()': u'pyeole.log.LevelsFilter',
                  u'levels': [ logging.INFO ],},
                 u'warning':
                 {u'()': u'pyeole.log.LevelsFilter',
                  u'levels' : [ logging.WARNING ],},
                 u'error':
                 {u'()': u'pyeole.log.LevelsFilter',
                  u'levels' : [ logging.ERROR ],},
                 u'critical':
                 {u'()': u'pyeole.log.LevelsFilter',
                  u'levels' : [ logging.CRITICAL ],},
                  # Optimise by grouping in one filter for stderr
                 u'stderr':
                 {u'()': u'pyeole.log.LevelsFilter',
                  u'levels' : [ logging.DEBUG,
                               logging.WARNING,
                               logging.ERROR,
                               logging.CRITICAL ],},
}


# Initialize an empty root logger to get errors if user does not
# configure logging.  I really think this should be done by
# :class:`logging.Logger` objects in the same way
# :func:`logging.error` and co. does.

if len(logging.getLogger().handlers) == 0:
    logging.basicConfig()

getLogger = logging.getLogger
"""Alias of logging.getLogger for user convenience.

Users does not need to import logging themself.

:param name: name of the logger object
:type name: `str`
"""

#@deprecated(message="use pyeole.log.getLogger.")
@deprecated
def PyEoleLog(name=None):
    return getLogger(name)

class LevelsFilter(logging.Filter):
    """Filter to accept records if its severity is in a specified list.

    """
    def __init__(self, levels=None):
        """
        :param levels: levels of accepted records
        :type levels: `list`
        """
        if levels is None:
            levels = []

        self._levels = levels
        logging.Filter.__init__(self)

    def filter(self, rec):
        return rec.levelno in self._levels

class LevelRangeFilter(LevelsFilter):
    """Filter to accept records if its severity is in specified range.

    """
    def __init__(self, start, end):
        """
        :param start: lower severity accepted
        :type start: `int`
        :param end: higher severity accepted
        :type end: `int`
        """
        LevelsFilter.__init__(levels=[start, end+1])

def init_logging(name=None, level=u'ERROR', as_root=False,
                 console=True, syslog=False, filename=None, smtp=None, config=None, mode='a'):
    """Initialize a logger.

    If :data:`name` is neither ``None`` nor ``root``, an unpropagated
    user logger is defined and any ``root`` logger is suppressed.

    You can define several loggers per module with a ``root`` one to
    catch unhandled message by defining the ``root`` one last.

    Simple configuration of console and syslog logging are achieved by
    passing :data:`console` and/or :data:`syslog` to `True`.

    Logging to a file is achieved by passing a filename to
    :data:`filename`.

    The console logger use colors on `tty` with
    :class:`pyeole.loghandlers.ColorizingStreamHandler`.

    You can customize the console logging by passing a list to
    :data:`console`, the default console configuration is
    ``[u'stdout', u'stderr', u'stddebug']``.

    You can customize the syslog logging by passing a dictionary to
    :data:`syslog`, the default syslog formatter is called ``syslog``.

    A full custom configuration of the logger can be provided by the
    :data:`config` parameter, in that case the :data:`name` must
    match the logger in the configuration (with `None` for root
    logger).

    Example::

        >>> from pyeole.log import init_logging, set_formatter
        >>> submodule_log = init_logging(name=u'submodule', level=u'warning',
                                         console=False,
                                         filename=u'/var/log/submodule.log')
        >>> log = init_logging(name=u'myprog', level=u'info')
        >>> root_log = init_logging(level=u'error')

    :param name: name of the logger, used as program name in syslog
                 message
    :type name: `str`
    :param level: severity of the logger
    :type level: `str`
    :param as_root: make logger a root logger even if name is not None
    :type as_root: `bool`
    :param console: activate logging to console
    :type console: `bool` or `list`
    :param syslog: activate logging to syslog
    :type syslog: `bool` or `dict`
    :param filename: activate logging to a file
    :type filename: `str` or `dict`
    :param config: personalized configuration to be used by
                  :func:`logging.config.dictConfig`
    :type config: `dict`

    """
    # dictConfig configuration
    log_config = {}

    if config is not None:
        log_config = config
    else:
        if console in [False, None] and syslog in [False, None] \
           and filename is None and not smtp:
            raise ValueError("You must select at least one log handler")

        # List of handlers to activate
        handlers = []

        # Configuration of handlers
        handlers_dict = {}

        # Configuration of formatters
        formatters_dict = _FORMATTERS_DICT.copy()

        # Configuration of filters
        filters_dict = _FILTERS_DICT.copy()

        # Logging to console
        if console not in [None, False]:
            if console == True:
                # Default configuration
                handlers.extend([u'stdout', u'stderr'])
                handlers_dict.update({u'stdout': _HANDLERS_DICT[u'stdout'],
                                      u'stderr': _HANDLERS_DICT[u'stderr']})
                if level.upper() == u'DEBUG':
                    handlers.append(u'stddebug')
                    handlers_dict.update({u'stddebug':
                                          _HANDLERS_DICT[u'stddebug']})
            elif isinstance(console, list):
                # User defined configuration
                handlers.extend( console )
                for output in console:
                    if output not in _HANDLERS_DICT:
                        msg = u"Unknown handler {0}"
                        raise ValueError(msg.format(output))

                    handlers_dict.update({output: _HANDLERS_DICT[output]})

        # Logging to file
        if isinstance(filename, str) or isinstance(filename, unicode):
            handlers.append(u'file')
            # Fix filename
            file_dict = _HANDLERS_DICT[u'file']
            file_dict.update({u'filename': filename})
            file_dict.update({u'mode': mode})
            handlers_dict.update({u'file': file_dict})

        if syslog not in [None, False]:
            # Activate syslog handler
            handlers.append(u'syslog')
            syslog_dict = _HANDLERS_DICT[u'syslog']

            # Configure syslog formatter
            if name is not None:
                syslog_dict.update({u'formatter': u'syslog-named'})


            if isinstance(syslog, dict):
                # User defined configuration
                handlers_dict.update({u'syslog': syslog.copy()})
            else:
                # Default configuration
                handlers_dict.update({u'syslog': syslog_dict})

        if smtp:
            handlers.append(u'smtp')
            smtp_dict = _HANDLERS_DICT[u'smtp'].copy()

            if isinstance(smtp, dict):
                smtp_dict.update(smtp)
                handlers_dict.update({u'smtp': smtp_dict})


        user_logger = {u'level': level.upper(),
                       u'handlers': handlers,
                       u'propagate': False, # Avoid duplicated messages
                   }

        # Do not disable library loggers
        log_config = {u'version': 1,
                      u'filters': filters_dict,
                      u'formatters': formatters_dict,
                      u'handlers': handlers_dict,
                      u'disable_existing_loggers': False,
        }

        if name in [None, u'root'] or as_root:
            log_config.update({u'root': user_logger})
        else:
            # Remove root handlers
            log_config.update({u'root': {u'handlers': []},
                               u'loggers': {name: user_logger}})

    logging.config.dictConfig(log_config)
    return logging.getLogger(name)


def set_formatter(logger, handler, formatter):
    """Set formatter name for a handler

    The :data:`handler` name is looked up in :data:`logger` handlers.

    The :data:`formatter` name is looked up in private formatter
    dictionary :data:`_FORMATTERS_DICT` to define a new
    :class:`logging.Formatter` and use it as :data:`handler` formatter.

    Example::

        >>> from pyeole.log import init_logging, set_formatter
        >>> log = init_logging(level=u'info')
        >>> set_formatter(log, u'stderr', u'brief')
        >>> log.warning(u"ATTENTION: be carefull")

    :param logger: logger object
    :type logger: `logging.Logger`
    :param handler: name of the handler
    :type handler: `str`
    :param formatter: name of the formatter
    :type formatter: `str`
    :raise ValueError: if :data:`formatter` is unknown
    :raise LookupError: if :data:`handler` is not found in :data:`logger`

    """
    if formatter not in _FORMATTERS_DICT:
        raise ValueError(u"Unknown formatter {0}".format(formatter))

    msg_format = _FORMATTERS_DICT[formatter][u'format']
    date_format = _FORMATTERS_DICT[formatter][u'datefmt']

    formatter = logging.Formatter(msg_format, date_format)

    for hdlr in logger.handlers:
        if hdlr.name == handler:
            return hdlr.setFormatter(formatter)

    raise LookupError(u"Handler not found: {0}".format(handler))


def set_filters(logger, handler, filters):
    """Set filters name for a log handler

    The :data:`handler` name is looked up in :data:`logger` handlers.

    The :data:`filters` names are looked up in private filters
    dictionary :data:`_FILTERS_DICT` to define a new
    :class:`logging.Filter` and use it as :data:`handler` filter.

    Example::

        >>> from pyeole.log import init_logging, set_filters
        >>> log = init_logging(level=u'debug', console=[u'stdout', u'stderr'],
                               filename=u'/tmp/debug.log')
        >>> set_filters(log, u'file', [u'debug'])
        >>> log.warning(u"ATTENTION: this message is display on stderr")
        >>> log.debug(u"ATTENTION: this message is output in /tmp/debug.log")

    :param logger: logger object
    :type logger: `logging.Logger`
    :param handler: name of the handler
    :type handler: `str`
    :param filters: names of filters
    :type filters: `list` of `str`
    :raise LookupError: if :data:`handler` is not found in
                        :data:`logger` or one of filters is not known.

    """
    levels = []
    for name in filters:
        if name not in _FILTERS_DICT:
            raise LookupError(u"Unknown filter name {0}".format(name))
        levels.extend(_FILTERS_DICT[name][u'levels'])

    # Uniq names
    levels = list(set(levels))
    new_filter = LevelsFilter(levels=levels)

    for hdlr in logger.handlers:
        if hdlr.name == handler:
            hdlr.filters = []
            return hdlr.addFilter(new_filter)

    raise LookupError(u"Handler not found: {0}".format(handler))


class trace(advice):
    """Decorator to trace function calls and returned value.

    It can replace positional and/or keyword arguments of traced
    function by some 'X' in log messages.

    The global :data:`ENABLE_TRACE` must be `True` for tracing to
    happens.

    """

    def __init__(self, decorated=None, hide_args=None, hide_kwargs=None):
        """Initialize the trace decorator.

        Accepts only keyword parameters, the :data:`faulty_positional`
        argument is here to detect if positional argument was passed.

        :param hide_args: positional argument indexes to replace
        :type hide_args: `list`
        :param hide_kwargs: keyword argument names to replace
        :type hide_kwargs: `list`

        """
        if decorated is not None and not callable(decorated):
            raise ValueError("Only keyword arguments authorized")

        # Traced function
        self.traced = None
        # Logger for tracing
        self.log = None

        if hide_args is None:
            self.hide_args = []
        else:
            self.hide_args = hide_args

        if hide_kwargs is None:
            self.hide_kwargs = []
        else:
            self.hide_kwargs = hide_kwargs

        return advice.__init__(self, before=self._log_call,
                               after=self._log_return, decorated=decorated)

    def _log_call(self, decorated, *args, **kwargs):
        """Log function calls.

        Do nothing if :data:`ENABLE_TRACE` is `False`.

        :param decorated: function or method to trace
        :type decorated: `function` or `instancemethod`
        :param args: positional arguments for the traced function or
                     method
        :type args: `tuple`
        :param args: keyword arguments for the traced function or
                     method
        :type args: `dict`

        """
        # Do nothing if tracing is not enabled
        if not ENABLE_TRACE:
            return

        # Get or initialize the caller logger
        # Take care to reinitialize the logger name depending on the caller
        if self.log is None or self.caller != self.log.name:
            # In before advice decorated function is 4 stacklevels up
            self.traced = format_caller(get_caller_infos(stacklevel=4))
            self.log = getLogger(self.traced)

        args_list = list(args[:])
        args_dict = kwargs.copy()
        for index in self.hide_args:
            if index < len(args):
                args_list[index] = 'XXXX'
        for keyname in self.hide_kwargs:
            if keyname in kwargs:
                args_dict[keyname] = 'XXXX'

        self.log.debug("trace: call '%s(%s, %s)'", decorated.__name__,
                       args_list, args_dict)

    def _log_return(self, ret, decorated, *args, **kwargs):
        """Log return values.

        Do nothing if :data:`ENABLE_TRACE` is `False`.

        :param ret: returned code of the traced function or method
        :type ret: `object`
        :param decorated: function or method to trace
        :type decorated: `function`
        :param args: positional arguments for the traced function or
                     method
        :type args: `tuple`
        :param args: keyword arguments for the traced function or
                     method
        :type args: `dict`

        """
        # Do nothing if tracing is not enabled
        if not ENABLE_TRACE:
            return
        self.log.debug("trace: return '%s'", ret)

####
#### For compatibility
####

EOLE_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - '%(funcName)s' %(message)s"
DEFAULT_LOG_FILE = "/var/log/eole/default.log"
DEFAULT_LOG_LEVEL = 'ERROR'

class DebugFilter(logging.Filter):
    def filter(self, rec):
        if rec.levelno <= logging.DEBUG:
            return logging.Filter.filter(self, rec)

class InfoFilter(logging.Filter):
    def filter(self, rec):
        if logging.INFO >= rec.levelno > logging.DEBUG:
            return logging.Filter.filter(self, rec)

class DebugInfoFilter(logging.Filter):
    def filter(self, rec):
        if logging.INFO >= rec.levelno:
            return logging.Filter.filter(self, rec)

class ErrorFilter(logging.Filter):
    def filter(self, rec):
        if rec.levelno >= logging.WARNING:
            return logging.Filter.filter(self, rec)

def get_formatter(logfmt, datefmt):
    if datefmt:
        return logging.Formatter(logfmt, datefmt)
    return logging.Formatter(logfmt)

def get_log_level(level=DEFAULT_LOG_LEVEL):
    """
        permet de renvoyer un objet loglevel avec un comportement standard

        :param level: string in
                      ['CRITICAL', 'ERROR', 'WARNING', 'WARN', 'INFO', 'DEBUG']
        :return: logging.loglevel
    """
    level = level.upper()
    if level in ['CRITICAL', 'ERROR', 'WARNING', 'WARN', 'INFO', 'DEBUG']:
        return getattr(logging, level)
    return logging.INFO

def make_file_handler(logfile, loglevel, logfilter, logformat, datefmt):
    """

        créee un handler de type FileHandler
        :param logfile: le fichier de log
        :param loglevel: (string) le niveau de sévérité ['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG']
        :param logfilter: class filtre à appliquer au handler
        :param logformat: (string) format du log
        :return: le handler

    """
    handler = logging.FileHandler(logfile)
    handler.setFormatter(get_formatter(logformat, datefmt))
    handler.setLevel(get_log_level(loglevel))
    if logfilter:
        handler.addFilter(logfilter)
    return handler

def make_logger(packagename, logfile=DEFAULT_LOG_FILE, errfile=None, debugfile=None, loglevel=DEFAULT_LOG_LEVEL, splitlog=False, logfmt=EOLE_FORMAT, datefmt=None):
    """

        point d'entrée principal du module pour la création du loggeur
        :Return: le logger

        Initialise un logger pour le package packagename
        :param packagename: (string) nom du package appelant ex: creole.eosfunc
        :param logfile: (string) fichier de log du package
        :param errfile: (string) optionnel, fichier de log des erreurs (CRITICAL & ERROR)
        :param debugfile: (string) optionnel, fichier de log du debug (DEBUG)
        :param loglevel: (string) niveau demandé
        :param splitlog: (boolean) optionnel, séparer les niveau de log suivant les fichiers ou concaténer les niveaux inférieurs
        :param logfmt: (string) optionnel, format du log (http://docs.python.org/library/logging.html#formatter-objects)

    """
    logger = logging.getLogger(packagename)
    logfilter = None
    # si déjà existant, le renvoie
    if logger.handlers:
        logger.setLevel(get_log_level(loglevel))
        return logger
    if errfile:
        if splitlog:
            logfilter = ErrorFilter()
        logger.addHandler(make_file_handler(errfile, 'WARNING', logfilter, logfmt, datefmt))
    if debugfile:
        if splitlog:
            logfilter = DebugFilter()
        # Si on a un fichier pour le DEBUG, on log à part
        logger.addHandler(make_file_handler(debugfile, 'DEBUG', logfilter, logfmt, datefmt))
        if splitlog:
            logfilter = InfoFilter()
        logger.addHandler(make_file_handler(logfile, 'INFO', logfilter, logfmt, datefmt))
    else:
        if splitlog:
            logfilter = DebugInfoFilter()
        # Sinon on log DEBUG+INFO dans le même fichier
        logger.addHandler(make_file_handler(logfile, 'DEBUG', logfilter, logfmt, datefmt))
    logger.setLevel(get_log_level(loglevel))
    return logger

