""" **Description**

        A log instance formats and writes log entries with a specified level
        and stream using the loguru package. Log entries contain an ISO-8601
        datetime and level.  Dynamic stream redirection and level specification
        are supported.

        Log uses lazy initialization to coexist with loguru, and removes or
        creates loguru handlers only on explicit stream assignment or write.
        In lazy initialization an existing default loguru handler, with an
        identity equal to 0, and a stream assignment of sys.stdout is removed,
        and a new loguru handler with a stream assignment of sys.stdout and a
        level of 'INFO' is created.

        In stream assignments subsequent to initialization, only loguru
        handlers previously created by Log will be removed, as the design
        pattern does not support multicast.  The ability to create and modify
        externally defined loguru handlers, multicast, and utilize any native
        loguru functionality is supported.

        Levels defined by loguru are supported, including custom definitions,
        which may have an associated numerical value greater than or equal to
        zero.  The level may be dynamically modified without creating,
        deleting, or modifying a loguru handler.

        Singleton.

        Thread safe.

    **Example**

        ::

            from diamondback import Log
            import io
            import numpy
            import sys


            try :

                # Set Log level to 'INFO', the default level.

                Log.level( 'INFO' )

                Log.write( 'INFO', 'Test Log write.' )

                # Set Log stream to sys.stdout.

                Log.stream( sys.stdout )

                Log.write( 'INFO', f'Valid = {True}' )

                # Set Log stream to a memory stream.

                stream = io.StringIO( )

                Log.stream( stream )

                x = numpy.random.rand( 2, 2 )

                Log.write( 'INFO', f'X = {x}' )

                # Read and reset memory stream.

                value = stream.getvalue( )

                _, _ = stream.truncate( 0 ), stream.seek( 0 )

                # Set Log stream to a file.

                with open( 'log.000.txt', 'w' ) as fout :

                    Log.stream( fout )

                    x = numpy.random.rand( 2, 2 )

                    Log.write( 'WARNING', f'X = {x}' )

            except Exception as ex :

                Log.write( 'ERROR', ex )

    **License**

        `BSD-3C. <https://github.com/larryturner/diamondback/blob/master/license>`_

        © 2018 - 2021 Larry Turner, Schneider Electric Industries SAS. All rights reserved.

    **Author**

        Larry Turner, Schneider Electric, Analytics & AI, 2018-03-22.

    **Definition**

"""

from loguru import logger
from threading import RLock
import numpy
import os
import sys
import typing


class Log( object ) :

    """ Log.
    """

    numpy.set_printoptions( formatter = { 'float' : '{:.6f}'.format } )

    _identity, _level = 0, logger.level( 'INFO' )

    _rlock = RLock( )

    @classmethod
    def level( cls, level : str ) -> None :

        """ Level.

            Arguments :

                level : str - in ( 'CRITICAL', 'ERROR', 'WARNING', 'SUCCESS', 'INFO', 'DEBUG', 'TRACE', ... < custom > ).
        """

        with ( Log._rlock ) :

            try :

                Log._level = logger.level( level.upper( ) )

            except Exception :

                raise ValueError( f'Level = {level}' )

    @classmethod
    def stream( cls, stream : typing.Any ) -> None :

        """ Stream.

            Arguments :

                stream : typing.Any, hasattr( 'write' ) - in ( sys.stderr, sys.stdout, open( < path >, 'w' or 'a' ) ).
        """

        with ( Log._rlock ) :

            if ( ( not stream ) or ( not hasattr( stream, 'write' ) ) ) :

                raise ValueError( f'Stream = {stream}' )

            try :

                logger.remove( Log._identity )

            except Exception :

                pass

            Log._identity = logger.add( stream, level = 0, format = '<blue>{time:YYYY-MM-DDTHH:mm:ss.SSZ}</blue> <level>{level}</level> {message}' )

    @classmethod
    def write( cls, level : str, entry : typing.Union[ str, Exception ] ) -> None :

        """ Formats and writes log entries using the loguru package with a
            specified level and stream.  Log entries contain an ISO-8601
            datetime and level.

            Arguments :

                level : str - in ( 'CRITICAL', 'ERROR', 'WARNING', 'SUCCESS', 'INFO', 'DEBUG', 'TRACE', ... < custom > ).

                entry : typing.Union[ str, Exception ].
        """

        with ( Log._rlock ) :

            if ( not Log._identity ) :

                Log.stream( sys.stdout )

            try :

                level = logger.level( level.upper( ) )

            except Exception :

                raise ValueError( f'Level = {level}' )

            if ( level.no >= Log._level.no ) :

                try :

                    if ( isinstance( entry, Exception ) ) :

                        entry = f'Exception = {type( entry ).__name__} {entry}'

                        info = sys.exc_info( )[ -1 ]

                        while ( info ) :

                            entry += f' @ File = {info.tb_frame.f_code.co_filename.split( os.sep )[ -1 ]} Line = {info.tb_lineno}'

                            info = info.tb_next

                    logger.log( level.name, entry )

                except Exception :

                    pass
