# Copyright (c) 2003-2005 Maxim Sobolev. All rights reserved.
# Copyright (c) 2006-2014 Sippy Software, Inc. All rights reserved.
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

from sippy.Signal import LogSignal
from time import time, localtime, strftime
from fcntl import flock, LOCK_EX, LOCK_UN
from signal import SIGUSR1
from threading import Thread, Condition

import sys, os, syslog
from functools import reduce

SIPLOG_DBUG = 0
SIPLOG_INFO = 1
SIPLOG_WARN = 2
SIPLOG_ERR = 3
SIPLOG_CRIT = 4

class AsyncLogger(Thread):
    log = None
    app = None
    master = None

    def __init__(self, app, master):
        Thread.__init__(self)
        self.master = master
        self.app = app
        self.setDaemon(True)
        self.start()

    def run(self):
        self.safe_open()
        while True:
            self.master.wi_available.acquire()
            while len(self.master.wi) == 0:
                self.master.wi_available.wait()
            op, args, kwargs = self.master.wi.pop(0)
            self.master.wi_available.release()
            if op == 'reopen':
                self.safe_open()
                continue
            if op == 'shutdown':
                self.closelog()
                return
            try:
                self.do_write(self.master.format(args, kwargs))
            except:
                # Reopen on any errror, drop message and continue
                self.safe_open()

    def do_write(self, obuf):
        my_flock = flock
        try:
            my_flock(self.log, LOCK_EX)
        except IOError as e:
            # Catch ENOTSUP
            if e.args[0] != 45:
                raise e
            my_flock = lambda x, y: None
        try:
            self.log.write(obuf)
            self.log.flush()
        except:
            pass
        my_flock(self.log, LOCK_UN)

    def safe_open(self):
        try:
            self.log = open(self.master.logfile, 'a')
        except Exception as e:
            print(e)

    def shutdown(self):
        self.master.wi_available.acquire()
        self.master.wi.append(('shutdown', None, None))
        self.master.wi_available.notify()
        self.master.wi_available.release()
        self.join()
        self.master = None

    def closelog(self):
        del self.log

class AsyncLoggerSyslog(AsyncLogger):
    def safe_open(self):
        try:
            syslog.openlog(self.app, syslog.LOG_PID)
        except Exception as e:
            print(e)

    def do_write(self, obuf):
        try:
            syslog.syslog(syslog.LOG_NOTICE, obuf)
        except Exception as e:
            print(e)
            pass

    def closelog(self):
        syslog.closelog()

class SipLogger(object):
    app = None
    call_id = None
    level = None
    write = None
    logfile = None
    discarded = 0
    pid = None
    logger = None
    signal_handler = None
    itime = None
    offstime = False

    def __init__(self, app, call_id = 'GLOBAL', logfile = '/var/log/sip.log'):
        self.itime = time()
        self.app = '/%s' % app
        self.call_id = call_id
        bend = os.environ.get('SIPLOG_BEND', 'stderr').lower()
        tform = os.environ.get('SIPLOG_TFORM', 'abs').lower()
        if tform == 'rel':
            self.offstime = True
            itime = os.environ.get('SIPLOG_TSTART', self.itime)
            self.itime = float(itime)
        self.level = eval('SIPLOG_' + os.environ.get('SIPLOG_LVL', 'INFO'))
        if bend == 'stderr':
            self.write = self.write_stderr
        elif bend == 'none':
            self.write = self.donoting
        else:
            self.write = self.write_logfile
            self.wi_available = Condition()
            self.wi = []
            if bend != 'syslog':
                self.logger = AsyncLogger(app, self)
                self.logfile = os.environ.get('SIPLOG_LOGFILE_FILE', logfile)
                self.signal_handler = LogSignal(self, SIGUSR1, self.reopen)
            else:
                self.logger = AsyncLoggerSyslog(app, self)
                self.app = ''

    def ftime(self, ltime):
        if self.offstime:
            ltime -= self.itime
        msec = (ltime % 1) * 1000
        if not self.offstime:
            return '%s.%.3d' % (strftime('%d %b %H:%M:%S', localtime(ltime)), msec)
        hrs = int(ltime / (60 * 60))
        ltime -= (hrs * 60 * 60)
        mins = int(ltime / 60)
        ltime -= (mins * 60)
        secs = int(ltime)
        return '%.2d:%.2d:%.2d.%.3d' % (hrs, mins, secs, msec)

    def donoting(self, *args, **kwargs):
        pass

    def write_stderr(self, *args, **kwargs):
        if kwargs.get('level', SIPLOG_INFO) < self.level:
            return
        sys.__stderr__.write(self.format(args, kwargs))

    def write_logfile(self, *args, **kwargs):
        if kwargs.get('level', SIPLOG_INFO) < self.level:
            return
        discarded = False
        self.wi_available.acquire()
        if len(self.wi) > 1000:
            # Discard some items, as the writer doesn't seems to be able
            # to keep up pace with incoming requests
            self.discarded += len(self.wi) - 1000
            self.wi = self.wi[-1000:]
            discarded = True
        self.wi.append(('write', args, kwargs))
        self.wi_available.notify()
        self.wi_available.release()
        if discarded and self.discarded % 1000 == 0:
            print('SipLogger: discarded %d requests, I/O too slow' % self.discarded)

    def format(self, args, kwargs):
        ltime = kwargs.get('ltime', None)
        if ltime == None:
            ltime = time()
        call_id = kwargs.get('call_id', self.call_id)
        if self.pid != None:
            pid = '[%d]' % self.pid
        else:
            pid = ''
        return '%s/%s%s%s: %s\n' % (self.ftime(ltime), \
          call_id, self.app, pid, \
          reduce(lambda x, y: x + y, [str(x) for x in args]))

    def reopen(self, signum = None):
        self.wi_available.acquire()
        self.wi.append(('reopen', None, None))
        self.wi_available.notify()
        self.wi_available.release()

    def shutdown(self):
        if self.logger == None:
            return
        if self.signal_handler != None:
            self.signal_handler.cancel()
            self.signal_handler = None
        self.logger.shutdown()
        self.logger = None

if __name__ == '__main__':
    log = SipLogger(sys.argv[1])
    log.write(sys.argv[2])
    log.shutdown()