from typing import Union, Any

import inspect
import traceback
import json
import os
from datetime import datetime
from enum import Enum


__all__ = ['LogLevel', 'Log', 'get_function_name']


class LogFormatter:
    @staticmethod
    def class_to_repr(formatted_key: str, value: Any) -> str:
        svalue: str = 'Object of type {} = '.format(str(type(value)))
        try:
            svalue += repr(value)
            if not svalue or (svalue.startswith('<') and svalue.endswith('>')):
                svalue = str(value)
        except Exception as error:
            svalue = f'Unable to stringify class: {error}'
        return '\n{}: {}'.format(formatted_key, svalue)

    @staticmethod
    def except_to_repr(value: BaseException) -> str:
        s: str = f'\nException = {value}\n!    Traceback =\n'
        return s + traceback.format_exc().replace('\n', '\n!    ')

    @staticmethod
    def dict_to_json(formatted_key: str, value: dict) -> str:
        return '\n{}: {}'.format(formatted_key, json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True))

    @staticmethod
    def dict_to_repr(formatted_key: str, value: dict) -> str:
        return '\n{}: {}'.format(formatted_key, repr(value))

    @staticmethod
    def get_formatted_kw_key(key: str) -> str:
        return '{}'.format(key.replace('_', ' ').title())

    @staticmethod
    def get_formatted_now() -> str:
        return datetime.now().strftime('%d/%m/%y %H:%M:%S')

    @staticmethod
    def get_formatted_module_function_couple(module: Union[str, None], function: Union[str, None]) -> str:
        s: str = ''
        if module and function:
            s += '{}.{}'.format(module, LogFormatter._format_function(function))
        elif module:
            s += LogFormatter._format_module(module)
        elif function:
            s += LogFormatter._format_function(function)

        return s

    @staticmethod
    def get_formatted_message(message: str) -> str:
        return message

    @staticmethod
    def _format_module(module: str) -> str:
        if '/' in module:
            module = os.path.splitext(module)[0].split('/')[-1]
        elif '.py' in module.lower():
            module = module.replace('.py', '').replace('.PY', '')

        return module.title()

    @staticmethod
    def _format_function(function: str) -> str:
        if not function.endswith('()'):
            function += '()'

        return function

    @staticmethod
    def get_module_name(file: str) -> str:
        """
        Returns the module name for the given __file__ value

        :param file: __file__
        :return: Module name
        """
        if file is None:
            return ''

        split_file = file.split(os.path.sep)

        return split_file[len(split_file) - 1].split('.')[0]

    @staticmethod
    def format_kw(**kwargs) -> str:
        s: str = ''
        for key in kwargs.keys():
            formatted_key: str = LogFormatter.get_formatted_kw_key(key)
            try:
                if kwargs[key] and isinstance(kwargs[key], dict):
                    s += LogFormatter.dict_to_json(formatted_key, kwargs[key])
                    continue
            except Exception as error:
                print('!!!! Exception that should not occur in LOGGER:', error)
                s += LogFormatter.dict_to_repr(formatted_key, kwargs[key])
                continue

            try:
                if kwargs[key] and isinstance(kwargs[key], BaseException):
                    s += '\n{}: {}'.format(formatted_key, LogFormatter.except_to_repr(kwargs[key]))
                elif kwargs[key] and type(kwargs[key]) == str:
                    s += '\n{}: "{}"'.format(formatted_key, kwargs[key].replace('\n', '\n\t'))
                elif kwargs[key] and isinstance(kwargs[key], object):
                    s += '\n{}: {}'.format(formatted_key, LogFormatter.class_to_repr(formatted_key, kwargs[key]))
                elif key == 'args':
                    if len(kwargs[key]) > 0:
                        s += '\n{}: {}'.format(formatted_key, str(kwargs[key]))
                elif key == 'kwargs' and len(kwargs.keys()) > 0:
                    if len(kwargs[key].keys()) > 0:
                        s += '\n{}: {}'.format(formatted_key, str(kwargs[key]))
                else:
                    s += '\n{}: {}'.format(formatted_key, kwargs[key])
            except BaseException as error:
                s += f'\nCannot parse {key}, {error}'
        return s


class LogLevel(Enum):
    """Log levels"""
    TEST = -1
    ALL = 0
    TRACE = 1
    DEBUG = 2
    SUCCESS = 3
    INFO = 4
    WARN = 5
    ERROR = 6
    SEVERE = 7
    FATAL = 8
    OFF = 9

    def __le__(self, other) -> bool:
        if isinstance(other, LogLevel):
            return self.value <= other.value
        elif isinstance(other, int):
            return self.value <= other

    def __lt__(self, other) -> bool:
        if isinstance(other, LogLevel):
            return self.value < other.value
        elif isinstance(other, int):
            return self.value < other

    def __eq__(self, other) -> bool:
        if isinstance(other, LogLevel):
            return self.value == other.value
        elif isinstance(other, LogLevel):
            return self.value == other

    def __ge__(self, other) -> bool:
        return not self <= other

    def __gt__(self, other) -> bool:
        return not self < other

    def __ne__(self, other) -> bool:
        return not self == other

    def __str__(self) -> str:
        return self.name


class Log:
    """Static logger"""
    LOGGER_NAME: str = 'EMPIRE'
    _log_level = LogLevel.INFO
    _log_buffer = None

    @staticmethod
    def test(message: str, module: str = None, function: str = None, **kwargs) -> None:
        Log.log(LogLevel.TEST, message, module, function, **kwargs)

    @staticmethod
    def trace(message: str, module: str = None, function: str = None, **kwargs) -> None:
        """Logs a trace"""
        Log.log(LogLevel.TRACE, message, module, function, **kwargs)

    @staticmethod
    def debug(message: str, module: str = None, function: str = None, **kwargs) -> None:
        """Logs a debug"""
        Log.log(LogLevel.DEBUG, message, module, function, **kwargs)

    @staticmethod
    def success(message: str, module: str = None, function: str = None, **kwargs) -> None:
        """Logs a success, just for you Yann"""
        Log.log(LogLevel.SUCCESS, message, module, function, **kwargs)

    @staticmethod
    def info(message: str, module: str = None, function: str = None, **kwargs) -> None:
        """Logs an info"""
        Log.log(LogLevel.INFO, message, module, function, **kwargs)

    @staticmethod
    def warn(message: str, module: str = None, function: str = None, **kwargs) -> None:
        """Logs a warning"""
        Log.log(LogLevel.WARN, message, module, function, **kwargs)

    @staticmethod
    def profile(message: str) -> None:
        """Logs a profiler message"""
        print(message)

    @staticmethod
    def error(message: str, module: str = None, function: str = None, **kwargs) -> None:
        """Logs an error"""
        Log.log(LogLevel.ERROR, message, module, function, **kwargs)

    @staticmethod
    def exception(message: str, exception: Exception, module: str = None, function: str = None, **kwargs):
        Log.error(message, module, function, exception=str(exception), traceback=traceback.format_exc(), **kwargs)

    @staticmethod
    def severe(message: str, module: str = None, function: str = None, **kwargs) -> None:
        """Logs an error"""
        Log.log(LogLevel.SEVERE, message, module, function, **kwargs)

    @staticmethod
    def fatal(message: str, module: str = None, function: str = None, **kwargs) -> None:
        """Logs a fatal"""
        Log.log(LogLevel.FATAL, message, module, function, **kwargs)

    @staticmethod
    def accumulate_logs() -> None:
        Log._log_buffer = ['****STARTING LOGGING****']

    @staticmethod
    def flush_logs() -> str:
        if Log._log_buffer is not None:
            Log._log_buffer.append('****END OF LOG****')
            return '\n'.join(Log._log_buffer)
        else:
            return ''

    @staticmethod
    def set_log_level(level: LogLevel) -> None:
        """Changes the log level"""
        Log._log_level = level

    @staticmethod
    def get_log_level() -> LogLevel:
        """Returns the current log level"""
        return Log._log_level

    @staticmethod
    def log(level: LogLevel, message: str, module: str, function: str, **kwargs) -> None:
        if Log._log_level <= level or level == LogLevel.TEST:
            log_string = Log._build_log_string(level, message, module, function, **kwargs)

            #################################
            #         DO NOT REMOVE         #
            #################################
            print(log_string)
            if Log._log_buffer is not None:
                Log._log_buffer.append(log_string)

    @staticmethod
    def _build_log_string(level: LogLevel, message: str, module: Union[str, None], function: Union[str, None], **kwargs) -> str:
        s: str = '[{}] ({}) [{}] [{}] {}'.format(LogFormatter.get_formatted_now(), Log.LOGGER_NAME, str(level), LogFormatter.get_formatted_module_function_couple(LogFormatter.get_module_name(module), function), LogFormatter.get_formatted_message(message))
        s += LogFormatter.format_kw(**kwargs)
        return s


def get_function_name() -> str:
    """Returns the current function name in which get_function_name is called"""
    frame = inspect.currentframe()
    function_name = inspect.getouterframes(frame)[1].function

    if function_name == '<module>':
        return '__root__'
    else:
        return function_name + '()'
