"""
A simple logger.

Simple usage:
    import logger
    log = logger.Log('my_log.log', logger.Log.DEBUG)
    log('A line in the log at the default level (DEBUG)')   # simple
    log('A log line at WARN level', logger.Log.WARN)        # hard to use
    log.info('log line issued at INFO level')               # best if using level

Based on the 'borg' recipe from [http://code.activestate.com/recipes/66531/].

Log levels styled on the Python 'logging' module.

Log output includes the module and line # of the log() call.
"""

import os
import sys
import datetime
import traceback


################################################################################
# A simple (?) logger.
################################################################################

class Log(object):

    __shared_state = {}                # this __dict__ shared by ALL instances

    # the predefined logging levels
    CRITICAL = 50
    ERROR = 40
    WARN = 30
    INFO = 20
    DEBUG = 10
    NOTSET = 0

    MAXLEVEL = CRITICAL
    MINLEVEL = NOTSET

    # dict to convert logging level back to symbolic name
    _level_num_to_name = {
                          NOTSET: 'NOTSET',
                          DEBUG: 'DEBUG',
                          INFO: 'INFO',
                          WARN: 'WARN',
                          ERROR: 'ERROR',
                          CRITICAL: 'CRITICAL',
                         }

    # default maximum length of filename (enforced)
    DefaultMaxFname = 15


    def __init__(self, logfile=None, level=NOTSET, append=False,
                 max_fname=DefaultMaxFname):
        """Initialise the logging object.

        logfile  the path to the log file
        level    logging level - don't log below this level
        append   True if log file is appended to
        """

        # make sure we have same state as all other log objects
        self.__dict__ = Log.__shared_state

        # set some initial state
        self.max_fname = max_fname
        self.sym_level = 'NOTSET'      # set in call to check_level()
        self.level = self.check_level(level)

        # if not given logfile name, make one up
        if logfile is None:
            logfile = '%s.log' % __name__

        # get correct options for rewrite or append of logfile
        log_options = 'w'
        if append:
            log_options = 'a'

        # test if we can use the file
        try:
            self.logfd = open(logfile, log_options)
            self.logfd.close()
        except IOError:
            # assume we have readonly filesystem
            basefile = os.path.basename(logfile)
            if sys.platform == 'win32':
                logfile = os.path.join('C:\\', basefile)
            else:
                logfile = os.path.join('~', basefile)

        # try to open logfile again
        self.logfd = open(logfile, log_options)

        self.logfile = logfile

        # announce time+date of opening logging and logging level
        self.debug('='*55)
        self.debug('Log started on %s, log level=%s'
                   % (datetime.datetime.now().ctime(),
                      self._level_num_to_name[level]))
        self.debug('-'*55)

        # finally, set some internal state
        self.set_level(self.level)

    def check_level(self, level):
        """Check the level value for legality.

        level  a numeric logging level

        If 'level' is invalid, raise Exception.  If valid, return value.
        """

        try:
            level = int(level)
        except ValueError:
            msg = "Logging level invalid: '%s'" % str(level)
            print(msg)
            raise Exception(msg)

        if not self.NOTSET <= level <= self.CRITICAL:
            msg = "Logging level invalid: '%s'" % str(level)
            print(msg)
            raise Exception(msg)

        return level

    def set_level(self, level):