# Copyright (c) 2017 Mark D. Hill and David A. Wood
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met: redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer;
# 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;
# neither the name of the copyright holders nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# 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
# OWNER 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.
#
# Authors: Sean Wilson

'''
This module supplies the global `test_log` object which all testing
results and messages are reported through.
'''
import testlib.wrappers as wrappers

class LogLevel():
    Fatal = 0
    Error = 1
    Warn  = 2
    Info  = 3
    Debug = 4
    Trace = 5


class RecordTypeCounterMetaclass(type):
    '''
    Record type metaclass.

    Adds a static integer value in addition to typeinfo so identifiers
    are common across processes, networks and module reloads.
    '''
    counter = 0
    def __init__(cls, name, bases, dct):
        cls.type_id = RecordTypeCounterMetaclass.counter
        RecordTypeCounterMetaclass.counter += 1


class Record(object, metaclass=RecordTypeCounterMetaclass):
    '''
    A generic object that is passed to the :class:`Log` and its handlers.

    ..note: Although not statically enforced, all items in the record should be
        be pickleable. This enables logging accross multiple processes.
    '''

    def __init__(self, **data):
        self.data = data

    def __getitem__(self, item):
        if item not in self.data:
            raise KeyError('%s not in record %s' %\
                    (item, self.__class__.__name__))
        return self.data[item]

    def __str__(self):
        return str(self.data)


class StatusRecord(Record):
    def __init__(self, obj, status):
        Record.__init__(self, metadata=obj.metadata, status=status)
class ResultRecord(Record):
    def __init__(self, obj, result):
        Record.__init__(self, metadata=obj.metadata, result=result)
#TODO Refactor this shit... Not ideal. Should just specify attributes.
class TestStatus(StatusRecord):
    pass
class SuiteStatus(StatusRecord):
    pass
class LibraryStatus(StatusRecord):
    pass
class TestResult(ResultRecord):
    pass
class SuiteResult(ResultRecord):
    pass
class LibraryResult(ResultRecord):
    pass
# Test Output Types
class TestStderr(Record):
    pass
class TestStdout(Record):
    pass
# Message (Raw String) Types
class TestMessage(Record):
    pass
class LibraryMessage(Record):
    pass


class Log(object):
    _result_typemap = {
        wrappers.LoadedLibrary.__name__: LibraryResult,
        wrappers.LoadedSuite.__name__: SuiteResult,
        wrappers.LoadedTest.__name__: TestResult,
    }
    _status_typemap = {
        wrappers.LoadedLibrary.__name__: LibraryStatus,
        wrappers.LoadedSuite.__name__: SuiteStatus,
        wrappers.LoadedTest.__name__: TestStatus,
    }

    def __init__(self, test=None):
        self.test = test
        self.handlers = []
        self._opened = False # TODO Guards to methods
        self._closed = False # TODO Guards to methods

    def finish_init(self):
        self._opened = True

    def close(self):
        self._closed = True
        for handler in self.handlers:
            handler.close()

    def log(self, record):
        if not self._opened:
            self.finish_init()
        if self._closed:
            raise Exception('The log has been closed'
                ' and is no longer available.')

        for handler in self.handlers:
            handler.handle(record)

    def message(self, message, level=LogLevel.Info, bold=False, **metadata):
        if self.test:
            record = TestMessage(message=message, level=level,
                test_uid=self.test.uid, suite_uid=self.test.parent_suite.uid)
        else:
            record = LibraryMessage(message=message, level=level,
                bold=bold, **metadata)

        self.log(record)

    def error(self, message):
        self.message(message, LogLevel.Error)

    def warn(self, message):
        self.message(message, LogLevel.Warn)

    def info(self, message):
        self.message(message, LogLevel.Info)

    def debug(self, message):
        self.message(message, LogLevel.Debug)

    def trace(self, message):
        self.message(message, LogLevel.Trace)

    def METHOD_NAME(self, obj, status):
        self.log(
            self._status_typemap[obj.__class__.__name__](obj, status))

    def result_update(self, obj, result):
        self.log(
            self._result_typemap[obj.__class__.__name__](obj, result))

    def add_handler(self, handler):
        if self._opened:
            raise Exception('Unable to add a handler once the log is open.')
        self.handlers.append(handler)

    def close_handler(self, handler):
        handler.close()
        self.handlers.remove(handler)

test_log = Log()