# -*- coding: utf-8 -*-
"""
Logging with timestamps and optional log files.

Print a timestamped message to a logfile, STDERR, or STDOUT.

If STDERR or STDOUT are used, colored flags are added.  Colored flags are INFO,
WARNINING, ERROR, or CRITICAL.

It is possible to write to both logfile and STDOUT/STDERR using the also_write
argument.

If level is 'error' or 'critical', error is written to STDERR unless also_write
== -1

MIN_LEVEL can also be provided, logs will only print if vlevel > MIN_LEVEL.
Level order: critical>error>warn>info>debug>verbose

Usage::

    import logme as lm
    lm.log("Screw up!", <outfile>,
        level='debug'|'info'|'warn'|'error'|'normal',
        also_write='stderr'|'stdout')

Example::

    lm.log('Hi')
    Prints: 20160223 11:46:24.969 | INFO --> Hi
    lm.log('Hi', level='debug')
    Prints nothing
    lm.MIN_LEVEL = 'debug'
    lm.log('Hi', level='debug')
    Prints: 20160223 11:46:24.969 | DEBUG --> Hi

Note: Uses terminal colors and STDERR, not compatible with non-unix systems
"""
import sys
import gzip
import bz2
import logging
from datetime import datetime as dt

__all__ = ['log', 'MIN_LEVEL', 'LOGFILE']

###################################
#  Constants for printing colors  #
###################################

WHITE  = '\033[97m'
YELLOW = '\033[93m'
RED    = '\033[91m'
BOLD   = '\033[1m'
ENDC   = '\033[0m'

MIN_LEVEL = 'info'
LOGFILE   = sys.stderr


def log(message, level='info', logfile=None, also_write=None,
        min_level=None, kind=None):
    """Print a string to logfile.

    Levels display as::

        verbose:  <timestamp> VERBOSE -->
        debug:    <timestamp> DEBUG -->
        info:     <timestamp> INFO -->
        warn:     <timestamp> WARNING -->
        error:    <timestamp> ERROR -->
        critical: <timestamp> CRITICAL -->

    Parameters
    ----------
    message : str, optional
        The message to print.
    logfile : file or logging object, optional
        Optional file to log to, defaults to STDERR. Can provide a logging
        object
    level : {'debug', 'info', 'warn', 'error', 'normal'}, optional
        Will only print if level > MIN_LEVEL
    also_write : {'stdout', 'stderr'}, optional
        Print to STDOUT or STDERR also.
        These only have an effect if the output is not already set to the same
        device.
    min_level : str, deprecated
        Retained for backwards compatibility, min_level should be set using the
        logme.MIN_LEVEL constant.
    kind : str, deprecated
        synonym for level, kept to retain backwards compatibility
    """
    stdout = False
    stderr = False
    message = str(message)

    if not logfile:
        logfile = LOGFILE

    if kind:
        level = kind

    min_level = min_level if min_level else MIN_LEVEL

    # Level checking, not used with logging objects
    level_map = {'verbose': 0, 'debug': 1, 'info': 2, 'warn': 3, 'error': 4,
                 'critical': 5,
                 'v': 0, 'd': 1, 'i': 2, 'w': 3, 'e': 4, 'c': 5,
                 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5}

    try:
        level = level_map[level]
    except KeyError:
        raise Exception('Invalid level {}'.format(level))

    try:
        min_level = level_map[min_level]
    except KeyError:
        raise Exception('Invalid min_level {}'.format(min_level))

    if level > 3:
        if also_write != -1 or also_write != 'stdout':
            also_write = 'stderr'

    # Attempt to handle all file types
    if isinstance(logfile, (logging.RootLogger, logging.Logger)):
        _logit(message, logfile, level, color=False, min_level=min_level)
    elif isinstance(logfile, str):
        with _open_zipped(logfile, 'a') as outfile:
            _logit(message, outfile, level, color=False, min_level=min_level)
    elif str(getattr(logfile, 'name')).strip('<>') == 'stdout':
        _logit(message, logfile, level, color=True, min_level=min_level)
        stdout = True
    elif str(getattr(logfile, 'name')).strip('<>') == 'stderr':
        _logit(message, logfile, level, color=True, min_level=min_level)
        stderr = True
    elif getattr(logfile, 'closed'):
        with _open_zipped(logfile.name, 'a') as outfile:
            _logit(message, outfile, level, color=False, min_level=min_level)
    else:
        _logit(message, logfile, level, color=False, min_level=min_level)

    # Also print to stdout or stderr if requested
    if also_write == 'stdout' and not stdout:
        _logit(message, sys.stdout, level, color=True, min_level=min_level)
    elif also_write == 'stderr' and not stderr:
        _logit(message, sys.stdout, level, color=True, min_level=min_level)


def clear(infile):
    """Truncate a file."""
    open(infile, 'w').close()


###############################################################################
#                             A Logging Exception                             #
###############################################################################


class LoggingException(Exception):

    """Log a critical message with logme and also raise."""

    def __init__(self, message, logfile=None):
        """Log message as critical, raise with first line."""
        args = {'kind': 'critical'}
        if logfile:
            args.update({'logfile': logfile})
        # Log with logme
        log(message, **args)
        # Raise with the first line of the log
        message = message.split('\n')[0]
        super(LoggingException, self).__init__(message)


###############################################################################
#                              Private Functions                              #
###############################################################################


def _logit(message, output, level, color=False, min_level=None):
    """Write message to file either with color or not.

    output must be filehandle or logging object.
    """
    now = dt.now()
    timestamp = "{0}.{1:<3}".format(now.strftime("%Y%m%d %H:%M:%S"),
                                    str(int(now.microsecond/1000)))

    flag_map  = {0: 'VERBOSE', 1: 'DEBUG', 2: 'INFO', 3: 'WARNING', 4: 'ERROR',
                 5: 'CRITICAL'}

    flag = flag_map[level]
    flag_len = len('{0} | {1} --> '.format(timestamp, flag)) - 2

    if color:
        flag = _color(flag)

    if isinstance(output, (logging.RootLogger, logging.Logger)):
        message = ' {} --> {}'.format(timestamp, message)
        if level == 0:
            output.debug(message)
        if level == 1:
            output.debug(message)
        if level == 2:
            output.info(message)
        if level == 3:
            output.warning(message)
        if level == 4:
            output.error(message)
        if level == 5:
            output.critical(message)
    else:
        # Check min_level before proceeding
        if level < min_level:
            return

        # Format multiline message
        lines = message.split('\n')
        if len(lines) != 1:
            message = lines[0] + '\n'
            lines = lines[1:]
            for line in lines:
                message = message + (''.ljust(flag_len, '-') + '> ' +
                                     line + '\n')
        output.write('{0} | {1} --> {2}\n'.format(timestamp, flag,
                                                  str(message)))


def _color(flag):
    """Return the flag with correct color codes."""
    if flag == 'VERBOSE':
        return flag
    elif flag == 'DEBUG':
        return flag
    elif flag == 'INFO':
        return BOLD + WHITE + flag + ENDC
    elif flag == 'WARNING':
        return BOLD + YELLOW + flag + ENDC
    elif flag == 'ERROR':
        return BOLD + RED + flag + ENDC
    elif flag == 'CRITICAL':
        return BOLD + RED + flag + ENDC
    else:
        raise Exception('Invalid flag type')


def _open_zipped(infile, mode='r'):
    """Return file handle of file regardless of zipped or not.

    Text mode enforced for compatibility with python2
    """
    mode   = mode[0] + 't'
    p2mode = mode
    if hasattr(infile, 'write'):
        return infile
    if isinstance(infile, str):
        if infile.endswith('.gz'):
            return gzip.open(infile, mode)
        if infile.endswith('.bz2'):
            if hasattr(bz2, 'open'):
                return bz2.open(infile, mode)
            else:
                return bz2.BZ2File(infile, p2mode)
        return open(infile, p2mode)
