#!/usr/bin/env python
# *****************************************************************************
#
# 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 2 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, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Module authors:
#   Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************


import os
from os.path import dirname, join
from logging import DEBUG, INFO, addLevelName
import mlzlog

from frappy.lib import generalConfig
from frappy.datatypes import BoolType
from frappy.properties import Property

OFF = 99
COMLOG = 15
addLevelName(COMLOG, 'COMLOG')
assert DEBUG < COMLOG < INFO
LOG_LEVELS = dict(mlzlog.LOGLEVELS, off=OFF, comlog=COMLOG)
LEVEL_NAMES = {v: k for k, v in LOG_LEVELS.items()}


def check_level(level):
    try:
        if isinstance(level, str):
            return LOG_LEVELS[level.lower()]
        if level in LEVEL_NAMES:
            return level
    except KeyError:
        pass
    raise ValueError(f'{level!r} is not a valid level')


class RemoteLogHandler(mlzlog.Handler):
    """handler for remote logging"""

    def __init__(self):
        super().__init__()
        self.subscriptions = {}  # dict[modname] of tuple(mobobj, dict [conn] of level)
        # None will be replaced by a callback when one is first installed
        self.send_log = None

    def emit(self, record):
        """unused"""

    def handle(self, record):
        modname = record.name.split('.')[-1]
        try:
            subscriptions = self.subscriptions[modname]
        except KeyError:
            return
        for conn, lev in subscriptions.items():
            if record.levelno >= lev:
                self.send_log(  # pylint: disable=not-callable
                    conn, modname, LEVEL_NAMES[record.levelno],
                    record.getMessage())

    def set_conn_level(self, modname, conn, level):
        level = check_level(level)
        subscriptions = self.subscriptions.setdefault(modname, {})
        if level == OFF:
            subscriptions.pop(conn, None)
        else:
            subscriptions[conn] = level

    def __repr__(self):
        return 'RemoteLogHandler()'


class LogfileHandler(mlzlog.LogfileHandler):

    def __init__(self, logdir, rootname, max_days=0):
        self.rootname = rootname
        self.max_days = max_days
        super().__init__(logdir, rootname)

    def emit(self, record):
        if record.levelno != COMLOG:
            super().emit(record)

    def getChild(self, name):
        child = type(self)(dirname(self.baseFilename), name, self.max_days)
        child.setLevel(self.level)
        return child

    def doRollover(self):
        super().doRollover()
        if self.max_days:
            # keep only the last max_days files
            with os.scandir(dirname(self.baseFilename)) as it:
                files = sorted(entry.path for entry in it if entry.name != 'current')
            for filepath in files[-self.max_days:]:
                os.remove(filepath)


class ComLogfileHandler(LogfileHandler):
    """handler for logging communication"""

    def format(self, record):
        return f'{self.formatter.formatTime(record)} {record.getMessage()}'


class HasComlog:
    """mixin for modules with comlog"""
    comlog = Property('whether communication is logged ', BoolType(),
                      default=True, export=False)
    _comLog = None

    def earlyInit(self):
        super().earlyInit()
        if self.comlog and generalConfig.initialized and generalConfig.comlog:
            self._comLog = mlzlog.Logger(f'COMLOG.{self.name}')
            self._comLog.handlers[:] = []
            directory = join(logger.logdir, logger.rootname, 'comlog', self.secNode.name)
            self._comLog.addHandler(ComLogfileHandler(
                directory, self.name, max_days=generalConfig.getint('comlog_days', 7)))

    def comLog(self, msg, *args, **kwds):
        self.log.log(COMLOG, msg, *args, **kwds)
        if self._comLog:
            self._comLog.info(msg, *args)


def init_remote_logging(log):
    '''Install RemoteLogHandler to the given logger.'''
    log.addHandler(RemoteLogHandler())


class MainLogger:
    def __init__(self):
        self.log = None
        self.logdir = None
        self.rootname = None
        self.console_handler = None

    def init(self, console_level='info'):
        self.rootname = generalConfig.get('logger_root', 'frappy')
        # set log level to minimum on the logger, effective levels on the handlers
        # needed also for RemoteLogHandler
        # modified from mlzlog.initLogging
        mlzlog.setLoggerClass(mlzlog.MLZLogger)
        assert self.log is None
        self.log = mlzlog.log = mlzlog.MLZLogger(self.rootname)

        self.log.setLevel(DEBUG)
        self.log.addHandler(mlzlog.ColoredConsoleHandler())

        self.logdir = generalConfig.get('logdir', '/tmp/log')
        if self.logdir:
            logfile_days = generalConfig.getint('logfile_days')
            logfile_handler = LogfileHandler(self.logdir, self.rootname, max_days=logfile_days)
            logfile_handler.setLevel(LOG_LEVELS[generalConfig.get('logfile_level', 'info')])
            self.log.addHandler(logfile_handler)
        self.log.handlers[0].setLevel(LOG_LEVELS[console_level])


logger = MainLogger()
